﻿//------------------------------------------------------------------------------
// <copyright file="NamedCommandService.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.ComponentModel.Design;
using System.Globalization;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.CommandBars;

namespace ModelingAddIns
{
    /// <summary>
    /// Control ID-s for some commonly used menus and toolbars (to avoid glob issues with command bars).
    /// </summary>
    public static class CommandBarId
    {
        public const string ClassDesignerToolBar = "Class Designer Toolbar";
        public const string ClassDesignerContextMenu = "Class Designer Context Menu";
        public const string ClassDesignerAddContextMenu = ClassDesignerContextMenu + "|Add";
        public const string ClassDiagramContextMenu = "Class Diagram Context Menu";
        public const string CodeWindowContextMenu = "Code Window";

        public const string MenuBar = "MenuBar";
        public const string EditMenu = MenuBar + "|Edit";
        public const string ViewMenu = MenuBar + "|View";
        public const string ViewOtherWindowsMenu = ViewMenu + "|Other Windows";
        public const string ClassDiagramMenu = MenuBar + "|ClassDiagram";
    }

    #region INamedCommandService

    /// <summary>
    /// A wrapping service for command support in add-ins.
    /// </summary>
    public interface INamedCommandService : IMenuCommandService
    {
        /// <summary>
        /// Given command text produces a full name of an add-in command.
        /// </summary>
        /// <param name="text">Command text.</param>
        /// <returns>Command name to be used with all calls to this service.</returns>
        string GetCanonicalName(string text);

        /// <summary>
        /// Finds CommandID by canonical command name.
        /// </summary>
        /// <param name="commandName">Canonical command name (produced by GetCanonicalName).</param>
        /// <returns>CommandID if command exists and null otherwise.</returns>
        CommandID FindCommandId(string commandName);

        /// <summary>
        /// Finds MenuCommand by canonical command name.
        /// </summary>
        /// <param name="commandName">Canonical command name (produced by GetCanonicalName).</param>
        /// <returns>MenuCommand if it was previously added and null otherwise.</returns>
        MenuCommand FindCommand(string commandName);

        /// <summary>
        /// Creates a new command with given name, text and description.
        /// </summary>
        /// <param name="commandName">Canonical command name (produced by GetCanonicalName).</param>
        /// <param name="text">Command text which will appear in UI.</param>
        /// <param name="description">Command description for UI.</param>
        /// <returns>Created commmand's CommandID.</returns>
        CommandID CreateCommand(string commandName, string text, string description);

        /// <summary>
        /// Places given command on a command bar at specified command bar UI path.
        /// </summary>
        /// <param name="commandName">Canonical command name (produced by GetCanonicalName).</param>
        /// <param name="commandBarPath">Command bar path.</param>
        void PlaceCommand(string commandName, string commandBarPath);

        /// <summary>
        /// Deletes specified command.
        /// </summary>
        /// <param name="commandName">Canonical command name (produced by GetCanonicalName).</param>
        void DeleteCommand(string commandName);

        /// <summary>
        /// Invokes specified command.
        /// </summary>
        /// <param name="commandName">Canonical command name (produced by GetCanonicalName).</param>
        /// <returns>Whether command was actually invoked.</returns>
        bool GlobalInvoke(string commandName);
    }

    #endregion

    /// <summary>
    /// Implements INamedCommandServices given IServiceProvider and AddIn objects.
    /// </summary>
    public sealed class NamedCommandService : INamedCommandService
    {
        #region Private data, constructor

        private AddIn m_addInInstance;
        private DTE2 m_dte;
        private Commands2 m_commands;
        private CommandBars m_commandBars;
        private Dictionary<string, MenuCommand> m_namedCommands;
        private Dictionary<CommandID, MenuCommand> m_menuCommands;

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="serviceProvider">Service provider capable of retreiving DTE service.</param>
        /// <param name="addInInstance">AddIn instance.</param>
        //[CLSCompliant(false)]
        public NamedCommandService(IServiceProvider serviceProvider, AddIn addInInstance)
        {
            if (serviceProvider == null) throw new ArgumentNullException("serviceProvider");
            m_dte = serviceProvider.GetService(typeof(EnvDTE.DTE)) as DTE2;
            if (m_dte == null) throw new ArgumentNullException("serviceProvider");

            if (addInInstance == null) throw new ArgumentNullException("addInInstance");
            m_addInInstance = addInInstance;
        }

        private AddIn AddInInstance
        {
            [DebuggerStepThrough]
            get { return m_addInInstance; }
        }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Commands2 Commands
        {
            [DebuggerStepThrough]
            get
            {
                if (m_commands == null)
                    m_commands = m_dte.Commands as Commands2;
                Debug.Assert(m_commands != null);
                return m_commands;
            }
        }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private CommandBars CommandBars
        {
            [DebuggerStepThrough]
            get
            {
                if (m_commandBars == null)
                    m_commandBars = m_dte.CommandBars as CommandBars;
                Debug.Assert(m_commandBars != null);
                return m_commandBars;
            }
        }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Dictionary<string, MenuCommand> NamedCommands
        {
            [DebuggerStepThrough]
            get
            {
                if (m_namedCommands == null)
                    m_namedCommands = new Dictionary<string, MenuCommand>();
                return m_namedCommands;
            }
        }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Dictionary<CommandID, MenuCommand> MenuCommands
        {
            [DebuggerStepThrough]
            get
            {
                if (m_menuCommands == null)
                    m_menuCommands = new Dictionary<CommandID, MenuCommand>();
                return m_menuCommands;
            }
        }

        #endregion

        #region Command type conversions

        private Command ConvertToCommand(CommandID commandId)
        {
            Debug.Assert(commandId != null);
            return this.Commands.Item('{' + commandId.Guid.ToString() + '}', commandId.ID);
        }

        private Command ConvertToCommand(string commandName)
        {
            Debug.Assert(!string.IsNullOrEmpty(commandName));
            return this.Commands.Item(commandName, -1);
        }

        private static CommandID ConvertToCommandID(Command command)
        {
            Debug.Assert(command != null);
            return new CommandID(new Guid(command.Guid), command.ID);
        }

        [Conditional("DEBUG")]
        private void LogDebug(string message)
        {
            if (m_addInInstance != null)
                Debug.WriteLine("[ NamedCommandService ] " + message + " (addin:" + m_addInInstance.Name + ")");
            else
                Debug.WriteLine("[ NamedCommandService ] " + message);
        }

        #endregion

        #region INamedCommandService Members

        public string GetCanonicalName(string text)
        {
            if (string.IsNullOrEmpty(text)) throw new ArgumentNullException("text");

            StringBuilder commandName = new StringBuilder(this.AddInInstance.ProgID);
            commandName.Append('.');

            int startLength = commandName.Length;
            for (int i = 0; i < text.Length; i++)
            {
                if (Char.IsLetterOrDigit(text[i]))
                    commandName.Append(text[i]);
            }

            if (commandName.Length == startLength) throw new FormatException();

            return commandName.ToString();
        }

        public CommandID FindCommandId(string commandName)
        {
            if (commandName == null) throw new ArgumentNullException("commandName");

            try
            {
                Command command = ConvertToCommand(commandName);
                Debug.Assert(command != null);

                return ConvertToCommandID(command);
            }
            catch (ArgumentException)
            {
                // There is no way to check for command presense besides catching this exception.
                // This is because Commands interface doesn't support something like Contains...
                return null;
            }
        }

        public MenuCommand FindCommand(string commandName)
        {
            if (string.IsNullOrEmpty(commandName)) throw new ArgumentException("Argument 'commandName' can't be a null or empty string.");

            MenuCommand command;
            this.NamedCommands.TryGetValue(commandName, out command);

            return command;
        }

        private static object[] _contextGUIDs = new object[0];

        public CommandID CreateCommand(string commandName, string text, string description)
        {
            if (string.IsNullOrEmpty(commandName)) throw new ArgumentNullException("commandName");
            if (string.IsNullOrEmpty(text)) throw new ArgumentNullException("text");
            if (description == null) throw new ArgumentNullException("description");

            LogDebug("Creating command '" + commandName + "'");

            // It's ok to pass in a prefixed name since that's what GetCanonicalName returns - we should
            // strip it out however before we pass it into AddNamedCommand2 since that adds the prefix itself.
            string prefix = this.AddInInstance.ProgID + '.';
            if (commandName.StartsWith(prefix))
                commandName = commandName.Remove(0, prefix.Length);

            Command command = this.Commands.AddNamedCommand2(
                this.AddInInstance,
                commandName,
                text,
                description,
                true,
                0,
                ref _contextGUIDs,
                (int)EnvDTE.vsCommandStatus.vsCommandStatusSupported,
                (int)EnvDTE80.vsCommandStyle.vsCommandStyleText,
                EnvDTE80.vsCommandControlType.vsCommandControlTypeButton);
            Debug.Assert(command != null);

            return ConvertToCommandID(command);
        }

        public void PlaceCommand(string commandName, string commandBarPath)
        {
            if (string.IsNullOrEmpty(commandName)) throw new ArgumentNullException("commandName");
            if (string.IsNullOrEmpty(commandBarPath)) throw new ArgumentNullException("commandBarPath");

            LogDebug("Adding command '" + commandName + "' to '" + commandBarPath + "'");

            Command command = ConvertToCommand(commandName);
            if (command == null)
                throw new InvalidOperationException("Command '" + commandName + "' doesn't exist and thus can't be placed in UI.");

            AddToCommandBar(command, commandBarPath);
        }

        public void DeleteCommand(string commandName)
        {
            if (string.IsNullOrEmpty(commandName)) throw new ArgumentNullException("commandName");

            LogDebug("Deleting command '" + commandName + "'");

            Command command = ConvertToCommand(commandName);
            if (command == null)
                throw new InvalidOperationException("Command '" + commandName + "' doesn't exist and thus can't be deleted.");

            command.Delete();
        }

        public bool GlobalInvoke(string commandName)
        {
            if (string.IsNullOrEmpty(commandName)) throw new ArgumentNullException("commandName");

            LogDebug("Invoking command '" + commandName + "'");

            MenuCommand command;
            if (this.NamedCommands.TryGetValue(commandName, out command))
            {
                command.Invoke();
                return true;
            }
            else
            {
                return false;
            }
        }

        #endregion

        #region IMenuCommandService Members

        // TODO: shall we delegate to the parent IMenuCommandService in case we're given not a named command?

        public void AddCommand(MenuCommand command)
        {
            if (command == null) throw new ArgumentNullException("command");

            Command dteCommand = ConvertToCommand(command.CommandID);
            Debug.Assert(dteCommand != null);

            LogDebug("Loading command '" + dteCommand.Name + "'");

            Debug.Assert(!NamedCommands.ContainsKey(dteCommand.Name));
            this.NamedCommands[dteCommand.Name] = command;

            Debug.Assert(!MenuCommands.ContainsKey(command.CommandID));
            this.MenuCommands[command.CommandID] = command;
        }

        public void RemoveCommand(MenuCommand command)
        {
            if (command == null) throw new ArgumentNullException("command");

            Command dteCommand = ConvertToCommand(command.CommandID);
            Debug.Assert(dteCommand != null);

            LogDebug("Unloading command '" + dteCommand.Name + "'");

            Debug.Assert(NamedCommands.ContainsKey(dteCommand.Name));
            this.NamedCommands.Remove(dteCommand.Name);

            Debug.Assert(MenuCommands.ContainsKey(command.CommandID));
            this.MenuCommands.Remove(command.CommandID);
        }

        public void AddVerb(DesignerVerb verb)
        {
            throw new NotSupportedException();
        }

        public MenuCommand FindCommand(CommandID commandID)
        {
            if (commandID == null) throw new ArgumentNullException("commandID");

            MenuCommand command;
            MenuCommands.TryGetValue(commandID, out command);

            return command;
        }

        public bool GlobalInvoke(CommandID commandID)
        {
            if (commandID == null) throw new ArgumentNullException("commandID");

            MenuCommand command;
            if (MenuCommands.TryGetValue(commandID, out command))
            {
                command.Invoke();
                return true;
            }
            else
            {
                return false;
            }
        }

        public void RemoveVerb(DesignerVerb verb)
        {
            throw new NotSupportedException();
        }

        public void ShowContextMenu(CommandID menuID, int x, int y)
        {
            throw new NotSupportedException();
        }

        public DesignerVerbCollection Verbs
        {
            get { throw new NotSupportedException(); }
        }

        #endregion

        #region Command placement methods

        private CommandBarControl AddToCommandBar(Command command, CommandBar commandBar, int position)
        {
            Debug.Assert(command != null);
            Debug.Assert(commandBar != null);

            foreach (CommandBarControl control in commandBar.Controls)
            {
                string guid;
                int id;
                this.Commands.CommandInfo(control, out guid, out id);

                if (guid == command.Guid && id == command.ID)
                    return control; // command is already there
            }

            return command.AddControl(commandBar, position) as CommandBarButton;
        }

        private struct CommandPathItem
        {
            internal string Name;
            internal int Position;
        }

        private static CommandPathItem[] ParseCommandBarPath(string commandBarPath)
        {
            string[] items = commandBarPath.Split('|');
            CommandPathItem[] pathItems = new CommandPathItem[items.Length];
            for (int i = 0; i < items.Length; i++)
            {
                string item = items[i].Trim();
                int startIndex = item.IndexOf('[');
                if (startIndex < 0)
                {
                    pathItems[i].Name = item;
                    pathItems[i].Position = -1;
                }
                else
                {
                    if (item[item.Length - 1] != ']') throw new FormatException();
                    pathItems[i].Name = item.Substring(0, startIndex);
                    pathItems[i].Position = Int32.Parse(item.Substring(startIndex + 1, item.Length - startIndex - 2), CultureInfo.InvariantCulture);
                }
                if (string.IsNullOrEmpty(pathItems[i].Name)) throw new FormatException();
            }
            return pathItems;
        }

        private IEnumerable<CommandBar> EnumerateCommandBars(CommandBar parent)
        {
            if (parent != null)
            {
                foreach (CommandBarControl control in parent.Controls)
                {
                    if (control.Type == MsoControlType.msoControlPopup)
                    {
                        CommandBarPopup popup = control as CommandBarPopup;
                        if (popup != null && popup.CommandBar != null)
                            yield return popup.CommandBar;
                    }
                }
            }
            else
            {
                foreach (CommandBar commandBar in this.CommandBars)
                    if (commandBar != null)
                        yield return commandBar;
            }
        }

        private CommandBar GetChildCommandBar(CommandBar parent, CommandPathItem index)
        {
            foreach (CommandBar commandBar in EnumerateCommandBars(parent))
            {
                if (commandBar.Name == index.Name)
                    return commandBar;
            }
            return null;
        }

        private void AddToCommandBar(Command command, string commandBarPath)
        {
            Debug.Assert(command != null);
            Debug.Assert(!string.IsNullOrEmpty(commandBarPath));

            CommandPathItem[] pathItems = ParseCommandBarPath(commandBarPath);

            CommandBar commandBar = null;
            int position = -1;
            for (int i = 0; i < pathItems.Length; i++)
            {
                CommandBar nextCommandBar = GetChildCommandBar(commandBar, pathItems[i]);
                if (nextCommandBar == null)
                {
                    nextCommandBar = this.Commands.AddCommandBar(
                        pathItems[i].Name,
                        vsCommandBarType.vsCommandBarTypeMenu,
                        commandBar,
                        position >= 0 ? position : commandBar.Controls.Count) as CommandBar;
                }
                Debug.Assert(nextCommandBar != null);
                commandBar = nextCommandBar;
                position = pathItems[i].Position;
            }

            AddToCommandBar(command, commandBar, position >= 0 ? position : commandBar.Controls.Count + 1);
        }

        #endregion
    }
}
